// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures;

internal static class MvcViewFeaturesLoggerExtensions
{
    private static readonly Action<ILogger, string, string[], Exception> _viewComponentExecuting;
    private static readonly Action<ILogger, string, double, string, Exception> _viewComponentExecuted;

    private static readonly Action<ILogger, string, double, Exception> _partialViewFound;
    private static readonly Action<ILogger, string, IEnumerable<string>, Exception> _partialViewNotFound;
    private static readonly Action<ILogger, string, Exception> _partialViewResultExecuting;
    private static readonly Action<ILogger, string, double, Exception> _partialViewResultExecuted;

    private static readonly Action<ILogger, string, Exception> _antiforgeryTokenInvalid;

    private static readonly Action<ILogger, string, Exception> _viewComponentResultExecuting;

    private static readonly Action<ILogger, string, Exception> _viewResultExecuting;
    private static readonly Action<ILogger, string, double, Exception> _viewResultExecuted;
    private static readonly Action<ILogger, string, double, Exception> _viewFound;
    private static readonly Action<ILogger, string, IEnumerable<string>, Exception> _viewNotFound;

    private static readonly Action<ILogger, string, Exception> _tempDataCookieNotFound;
    private static readonly Action<ILogger, string, Exception> _tempDataCookieLoadSuccess;
    private static readonly Action<ILogger, string, Exception> _tempDataCookieLoadFailure;

    private static readonly Action<ILogger, Type, Exception> _notMostEffectiveFilter;

    private static readonly LogDefineOptions SkipEnabledCheckLogOptions = new() { SkipEnabledCheck = true };

    static MvcViewFeaturesLoggerExtensions()
    {
        _viewComponentExecuting = LoggerMessage.Define<string, string[]>(
            LogLevel.Debug,
            new EventId(1, "ViewComponentExecuting"),
            "Executing view component {ViewComponentName} with arguments ({Arguments}).",
            SkipEnabledCheckLogOptions);

        _viewComponentExecuted = LoggerMessage.Define<string, double, string>(
            LogLevel.Debug,
            new EventId(2, "ViewComponentExecuted"),
            "Executed view component {ViewComponentName} in {ElapsedMilliseconds}ms and returned " +
            "{ViewComponentResult}",
            SkipEnabledCheckLogOptions);

        _partialViewResultExecuting = LoggerMessage.Define<string>(
            LogLevel.Information,
            new EventId(1, "PartialViewResultExecuting"),
            "Executing PartialViewResult, running view {PartialViewName}.");

        _partialViewFound = LoggerMessage.Define<string, double>(
            LogLevel.Debug,
            new EventId(2, "PartialViewFound"),
            "The partial view path '{PartialViewFilePath}' was found in {ElapsedMilliseconds}ms.");

        _partialViewNotFound = LoggerMessage.Define<string, IEnumerable<string>>(
            LogLevel.Error,
            new EventId(3, "PartialViewNotFound"),
            "The partial view '{PartialViewName}' was not found. Searched locations: {SearchedViewLocations}");

        _partialViewResultExecuted = LoggerMessage.Define<string, double>(
            LogLevel.Information,
            new EventId(4, "PartialViewResultExecuted"),
            "Executed PartialViewResult - view {PartialViewName} executed in {ElapsedMilliseconds}ms.");

        _antiforgeryTokenInvalid = LoggerMessage.Define<string>(
            LogLevel.Information,
            new EventId(1, "AntiforgeryTokenInvalid"),
            "Antiforgery token validation failed. {Message}");

        _viewComponentResultExecuting = LoggerMessage.Define<string>(
            LogLevel.Information,
            new EventId(1, "ViewComponentResultExecuting"),
            "Executing ViewComponentResult, running {ViewComponentName}.",
            SkipEnabledCheckLogOptions);

        _viewResultExecuting = LoggerMessage.Define<string>(
            LogLevel.Information,
            new EventId(1, "ViewResultExecuting"),
            "Executing ViewResult, running view {ViewName}.");

        _viewFound = LoggerMessage.Define<string, double>(
            LogLevel.Debug,
            new EventId(2, "ViewFound"),
            "The view path '{ViewFilePath}' was found in {ElapsedMilliseconds}ms.");

        _viewNotFound = LoggerMessage.Define<string, IEnumerable<string>>(
            LogLevel.Error,
            new EventId(3, "ViewNotFound"),
            "The view '{ViewName}' was not found. Searched locations: {SearchedViewLocations}");

        _viewResultExecuted = LoggerMessage.Define<string, double>(
            LogLevel.Information,
            new EventId(4, "ViewResultExecuted"),
            "Executed ViewResult - view {ViewName} executed in {ElapsedMilliseconds}ms.");

        _tempDataCookieNotFound = LoggerMessage.Define<string>(
            LogLevel.Debug,
            new EventId(1, "TempDataCookieNotFound"),
            "The temp data cookie {CookieName} was not found.");

        _tempDataCookieLoadSuccess = LoggerMessage.Define<string>(
            LogLevel.Debug,
            new EventId(2, "TempDataCookieLoadSuccess"),
            "The temp data cookie {CookieName} was used to successfully load temp data.");

        _tempDataCookieLoadFailure = LoggerMessage.Define<string>(
            LogLevel.Warning,
            new EventId(3, "TempDataCookieLoadFailure"),
            "The temp data cookie {CookieName} could not be loaded.");

        _notMostEffectiveFilter = LoggerMessage.Define<Type>(
            LogLevel.Trace,
            new EventId(1, "NotMostEffectiveFilter"),
            "Skipping the execution of current filter as its not the most effective filter implementing the policy {FilterPolicy}.");
    }

#nullable enable
    public static IDisposable? ViewComponentScope(this ILogger logger, ViewComponentContext context)
    {
        return logger.BeginScope(new ViewComponentLogScope(context.ViewComponentDescriptor));
    }
#nullable restore

    public static void ViewComponentExecuting(
        this ILogger logger,
        ViewComponentContext context,
        object[] arguments)
    {
        if (logger.IsEnabled(LogLevel.Debug))
        {
            var formattedArguments = GetFormattedArguments(arguments);
            _viewComponentExecuting(logger, context.ViewComponentDescriptor.DisplayName, formattedArguments, null);
        }
    }

    private static string[] GetFormattedArguments(object[] arguments)
    {
        if (arguments == null || arguments.Length == 0)
        {
            return Array.Empty<string>();
        }

        var formattedArguments = new string[arguments.Length];
        for (var i = 0; i < formattedArguments.Length; i++)
        {
            formattedArguments[i] = Convert.ToString(arguments[i], CultureInfo.InvariantCulture);
        }

        return formattedArguments;
    }

    public static void ViewComponentExecuted(
        this ILogger logger,
        ViewComponentContext context,
        TimeSpan timespan,
        object result)
    {
        // Don't log if logging wasn't enabled at start of request as time will be wildly wrong.
        if (logger.IsEnabled(LogLevel.Debug))
        {
            _viewComponentExecuted(
                logger,
                context.ViewComponentDescriptor.DisplayName,
                timespan.TotalMilliseconds,
                Convert.ToString(result, CultureInfo.InvariantCulture),
                null);
        }
    }

    public static void PartialViewFound(
        this ILogger logger,
        IView view,
        TimeSpan timespan)
    {
        _partialViewFound(logger, view.Path, timespan.TotalMilliseconds, null);
    }

    public static void PartialViewNotFound(
        this ILogger logger,
        string partialViewName,
        IEnumerable<string> searchedLocations)
    {
        _partialViewNotFound(logger, partialViewName, searchedLocations, null);
    }

    public static void PartialViewResultExecuting(this ILogger logger, string partialViewName)
    {
        _partialViewResultExecuting(logger, partialViewName, null);
    }

    public static void PartialViewResultExecuted(this ILogger logger, string partialViewName, TimeSpan timespan)
    {
        _partialViewResultExecuted(logger, partialViewName, timespan.TotalMilliseconds, null);
    }

    public static void AntiforgeryTokenInvalid(this ILogger logger, string message, Exception exception)
    {
        _antiforgeryTokenInvalid(logger, message, exception);
    }

    public static void ViewComponentResultExecuting(this ILogger logger, string viewComponentName)
    {
        if (logger.IsEnabled(LogLevel.Information))
        {
            _viewComponentResultExecuting(logger, viewComponentName, null);
        }
    }

    public static void ViewComponentResultExecuting(this ILogger logger, Type viewComponentType)
    {
        if (logger.IsEnabled(LogLevel.Information))
        {
            _viewComponentResultExecuting(logger, viewComponentType.Name, null);
        }
    }

    public static void ViewResultExecuting(this ILogger logger, string viewName)
    {
        _viewResultExecuting(logger, viewName, null);
    }

    public static void ViewResultExecuted(this ILogger logger, string viewName, TimeSpan timespan)
    {
        _viewResultExecuted(logger, viewName, timespan.TotalMilliseconds, null);
    }

    public static void ViewFound(this ILogger logger, IView view, TimeSpan timespan)
    {
        _viewFound(logger, view.Path, timespan.TotalMilliseconds, null);
    }

    public static void ViewNotFound(this ILogger logger, string viewName,
        IEnumerable<string> searchedLocations)
    {
        _viewNotFound(logger, viewName, searchedLocations, null);
    }

    public static void TempDataCookieNotFound(this ILogger logger, string cookieName)
    {
        _tempDataCookieNotFound(logger, cookieName, null);
    }

    public static void TempDataCookieLoadSuccess(this ILogger logger, string cookieName)
    {
        _tempDataCookieLoadSuccess(logger, cookieName, null);
    }

    public static void TempDataCookieLoadFailure(this ILogger logger, string cookieName, Exception exception)
    {
        _tempDataCookieLoadFailure(logger, cookieName, exception);
    }

    public static void NotMostEffectiveFilter(this ILogger logger, Type policyType)
    {
        _notMostEffectiveFilter(logger, policyType, null);
    }

    private class ViewComponentLogScope : IReadOnlyList<KeyValuePair<string, object>>
    {
        private readonly ViewComponentDescriptor _descriptor;

        public ViewComponentLogScope(ViewComponentDescriptor descriptor)
        {
            _descriptor = descriptor;
        }

        public KeyValuePair<string, object> this[int index]
        {
            get
            {
                if (index == 0)
                {
                    return new KeyValuePair<string, object>("ViewComponentName", _descriptor.DisplayName);
                }
                else if (index == 1)
                {
                    return new KeyValuePair<string, object>("ViewComponentId", _descriptor.Id);
                }
                throw new IndexOutOfRangeException(nameof(index));
            }
        }

        public int Count => 2;

        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            for (var i = 0; i < Count; ++i)
            {
                yield return this[i];
            }
        }

        public override string ToString()
        {
            return _descriptor.DisplayName;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}
